Hanye官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. <template>
  2. <div>
  3. <div class="w-full h-[55px] sm:h-[72px]"></div>
  4. <ErrorBoundary :error="error">
  5. <div v-if="isLoading" class="flex justify-center py-12">
  6. <!-- 加载中 -->
  7. <div
  8. class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"
  9. ></div>
  10. </div>
  11. <div v-else>
  12. <!-- 面包屑导航 -->
  13. <div class="max-w-full mb-6 xl:px-2 lg:px-2 md:px-4 px-4 mt-6">
  14. <div class="max-w-screen-2xl mx-auto">
  15. <nuxt-link
  16. to="/"
  17. class="justify-start text-white/60 text-base font-normal"
  18. >ホーム</nuxt-link
  19. >
  20. <span class="text-white/60 text-base font-normal px-2"> / </span>
  21. <nuxt-link
  22. to="/products"
  23. class="text-white/60 text-base font-normal"
  24. >製品一覧</nuxt-link
  25. >
  26. <span class="text-white/60 text-base font-normal px-2"> / </span>
  27. <nuxt-link
  28. v-if="product?.category"
  29. :to="`/products?category=${encodeURIComponent(product.category)}`"
  30. class="text-white/60 text-base font-normal"
  31. >{{ product.category }}</nuxt-link
  32. >
  33. <span class="text-white/60 text-base font-normal px-2"> / </span>
  34. <span class="text-white text-base font-normal">{{
  35. product?.title || product?.name
  36. }}</span>
  37. </div>
  38. </div>
  39. <!-- 产品详情内容 -->
  40. <div
  41. v-if="product"
  42. class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4"
  43. >
  44. <div class="max-w-screen-2xl mx-auto">
  45. <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
  46. <!-- 左侧产品图片 -->
  47. <div class="flex flex-col gap-6">
  48. <!-- 主图展示 -->
  49. <div
  50. class="bg-zinc-900 rounded-lg p-8 relative overflow-hidden group aspect-square"
  51. >
  52. <!-- 加载状态 -->
  53. <div
  54. v-if="isImageLoading"
  55. class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 z-10"
  56. >
  57. <div
  58. class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"
  59. ></div>
  60. </div>
  61. <!-- 主图容器 -->
  62. <div class="relative w-full h-full">
  63. <!-- 当前图片 -->
  64. <img
  65. :src="currentImage"
  66. :alt="product.name"
  67. class="absolute inset-0 w-full h-full object-contain rounded-lg transition-all duration-500"
  68. :class="{
  69. 'opacity-0': isImageLoading,
  70. 'opacity-100': !isImageLoading,
  71. }"
  72. @load="handleImageLoad"
  73. @error="handleImageError"
  74. />
  75. <!-- 预加载图片 -->
  76. <img
  77. v-if="preloadImage"
  78. :src="preloadImage"
  79. class="absolute inset-0 w-full h-full object-contain rounded-lg opacity-0"
  80. @load="handlePreloadComplete"
  81. />
  82. </div>
  83. <!-- 错误提示 -->
  84. <div
  85. v-if="imageError"
  86. class="absolute inset-0 flex items-center justify-center bg-red-900/50 z-20"
  87. >
  88. <div class="flex flex-col items-center gap-2">
  89. <span class="text-white"
  90. >画像の読み込みに失敗しました</span
  91. >
  92. <button
  93. @click.stop="retryLoadImage"
  94. class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors duration-300"
  95. >
  96. 再試行
  97. </button>
  98. </div>
  99. </div>
  100. </div>
  101. <!-- 缩略图列表 -->
  102. <div class="flex gap-4 overflow-x-auto pb-2 scrollbar-hide">
  103. <div
  104. v-for="(image, index) in [
  105. product.image,
  106. ...(product.gallery || []),
  107. ]"
  108. :key="index"
  109. @click="changeImage(image)"
  110. class="flex-shrink-0 w-20 h-20 cursor-pointer rounded-lg transition-all duration-300 relative group aspect-square p-0.5"
  111. :class="{
  112. 'bg-gradient-to-r from-blue-500 to-blue-600':
  113. currentImage === image,
  114. 'hover:bg-gradient-to-r hover:from-blue-500/50 hover:to-blue-600/50':
  115. currentImage !== image,
  116. 'opacity-50':
  117. isThumbnailLoading[index] || thumbnailErrors[index],
  118. }"
  119. >
  120. <!-- 缩略图加载状态 -->
  121. <div
  122. v-if="isThumbnailLoading[index]"
  123. class="absolute inset-0 flex items-center justify-center bg-zinc-800 rounded-lg"
  124. >
  125. <div
  126. class="animate-spin h-4 w-4 border-2 border-blue-500 rounded-full border-t-transparent"
  127. ></div>
  128. </div>
  129. <!-- 缩略图遮罩 -->
  130. <div
  131. class="absolute inset-0 bg-black/0 transition-all duration-300 rounded-lg"
  132. :class="{
  133. 'bg-black/30': currentImage === image,
  134. 'group-hover:bg-black/20': currentImage !== image,
  135. }"
  136. ></div>
  137. <img
  138. :src="image"
  139. :alt="`${product.name} - 画像 ${index + 1}`"
  140. class="w-full h-full object-cover transition-all duration-300 rounded-lg"
  141. :class="{
  142. 'opacity-0': isThumbnailLoading[index],
  143. 'opacity-100': !isThumbnailLoading[index],
  144. 'group-hover:scale-110': currentImage !== image,
  145. }"
  146. @load="handleThumbnailLoad(index)"
  147. @error="handleThumbnailError(index)"
  148. />
  149. <!-- 选中标记 -->
  150. <div
  151. v-if="currentImage === image"
  152. class="absolute top-1 right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center"
  153. >
  154. <div class="w-2 h-2 bg-white rounded-full"></div>
  155. </div>
  156. <!-- 缩略图错误提示 -->
  157. <div
  158. v-if="thumbnailErrors[index]"
  159. class="absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg"
  160. >
  161. <div class="flex flex-col items-center gap-1">
  162. <span class="text-white text-xs">エラー</span>
  163. <button
  164. @click.stop="retryLoadThumbnail(index)"
  165. class="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 transition-colors duration-300"
  166. >
  167. 再試行
  168. </button>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. <!-- 右侧产品信息 -->
  175. <div class="flex flex-col gap-8">
  176. <!-- 产品名称 -->
  177. <div class="bg-zinc-900 rounded-lg p-6">
  178. <h1 class="text-white text-3xl font-medium mb-4">
  179. {{ product.title || product.name }}
  180. </h1>
  181. <div class="text-stone-400 text-lg leading-relaxed">
  182. {{ product.summary }}
  183. </div>
  184. </div>
  185. <!-- 产品参数 -->
  186. <div class="bg-zinc-900 rounded-lg p-6">
  187. <h2 class="text-white text-xl font-medium mb-6">製品仕様</h2>
  188. <div class="grid grid-cols-1 gap-4">
  189. <div
  190. class="flex justify-between items-center py-2 border-b border-zinc-800"
  191. >
  192. <span class="text-stone-400">カテゴリー</span>
  193. <span class="text-white font-medium">{{
  194. product.category
  195. }}</span>
  196. </div>
  197. <div
  198. class="flex justify-between items-center py-2 border-b border-zinc-800"
  199. >
  200. <span class="text-stone-400">用途</span>
  201. <span class="text-white font-medium">{{
  202. product.usage?.join(", ")
  203. }}</span>
  204. </div>
  205. <div class="flex justify-between items-center py-2">
  206. <span class="text-stone-400">容量</span>
  207. <span class="text-white font-medium">{{
  208. product.capacities?.join(" / ")
  209. }}</span>
  210. </div>
  211. </div>
  212. </div>
  213. <!-- 产品描述 -->
  214. <div class="bg-zinc-900 rounded-lg p-6">
  215. <h2 class="text-white text-xl font-medium mb-6">产品描述</h2>
  216. <div
  217. class="text-stone-400 leading-relaxed space-y-4 prose prose-invert max-w-none"
  218. >
  219. {{ product.description }}
  220. </div>
  221. </div>
  222. <div class="bg-zinc-900 rounded-lg p-6">
  223. <h2 class="text-white text-xl font-medium mb-6">详细描述</h2>
  224. <div
  225. class="text-stone-400 leading-relaxed space-y-4 prose prose-invert max-w-none"
  226. >
  227. <ContentRenderer :value="product.content" />
  228. </div>
  229. </div>
  230. <!-- 相关产品 -->
  231. <div
  232. v-if="relatedProducts.length > 0"
  233. class="bg-zinc-900 rounded-lg p-6"
  234. >
  235. <h2 class="text-white text-xl font-medium mb-6">
  236. {{
  237. product.meta?.series && product.meta.series.length > 0
  238. ? "同シリーズ製品"
  239. : "関連製品"
  240. }}
  241. </h2>
  242. <div
  243. class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
  244. >
  245. <nuxt-link
  246. v-for="relatedProduct in relatedProducts"
  247. :key="relatedProduct.id"
  248. :to="`/products/${relatedProduct.id}`"
  249. class="group"
  250. >
  251. <div
  252. class="bg-zinc-800 rounded-lg p-4 transition-all duration-300 hover:bg-zinc-700"
  253. >
  254. <div
  255. class="aspect-square mb-4 overflow-hidden rounded-lg"
  256. >
  257. <img
  258. :src="relatedProduct.image"
  259. :alt="relatedProduct.title || relatedProduct.name"
  260. class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
  261. />
  262. </div>
  263. <h3
  264. class="text-white text-lg font-medium mb-2 line-clamp-2"
  265. >
  266. {{ relatedProduct.title || relatedProduct.name }}
  267. </h3>
  268. <p class="text-stone-400 text-sm line-clamp-2">
  269. {{ relatedProduct.summary }}
  270. </p>
  271. </div>
  272. </nuxt-link>
  273. </div>
  274. </div>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. </div>
  280. </ErrorBoundary>
  281. </div>
  282. </template>
  283. <script setup lang="ts">
  284. /**
  285. * 产品详情页面
  286. * 展示产品主图、参数和描述
  287. */
  288. import { useErrorHandler } from "~/composables/useErrorHandler";
  289. import { useAsyncData, useRoute, useI18n } from "#imports";
  290. import { queryCollection } from "#imports";
  291. import { ContentRenderer } from "#components";
  292. const { error, isLoading, wrapAsync } = useErrorHandler();
  293. const route = useRoute();
  294. const product = ref<Product | null>(null);
  295. const relatedProducts = ref<Product[]>([]);
  296. const { locale, t } = useI18n();
  297. const currentImage = ref<string>("");
  298. const isImageLoading = ref(true);
  299. const isThumbnailLoading = ref<boolean[]>([]);
  300. const imageError = ref(false);
  301. const thumbnailErrors = ref<boolean[]>([]);
  302. const preloadImage = ref<string | null>(null);
  303. interface Product {
  304. id: string;
  305. name: string;
  306. usage: string[];
  307. capacities: string[];
  308. category: string;
  309. description: string;
  310. summary: string;
  311. image: string;
  312. gallery: string[];
  313. body: string;
  314. content?: any;
  315. meta?: {
  316. series?: string[];
  317. name?: string;
  318. title?: string;
  319. image?: string;
  320. summary?: string;
  321. };
  322. title?: string;
  323. }
  324. interface ContentCollectionItem {
  325. _path?: string;
  326. _id?: string;
  327. name?: string;
  328. title?: string;
  329. usage?: string[];
  330. capacities?: string[];
  331. categoryId?: string;
  332. description?: string;
  333. summary?: string;
  334. image?: string;
  335. gallery?: string[];
  336. body?: string;
  337. content?: any;
  338. series?: string[];
  339. meta?: {
  340. series?: string[];
  341. name?: string;
  342. title?: string;
  343. image?: string;
  344. summary?: string;
  345. };
  346. }
  347. interface ContentData {
  348. path: string;
  349. name?: string;
  350. title?: string;
  351. usage?: string[];
  352. capacities?: string[];
  353. categoryId?: string;
  354. category?: string;
  355. description?: string;
  356. summary?: string;
  357. image?: string;
  358. gallery?: string[];
  359. body?: string;
  360. content?: any;
  361. meta?: {
  362. series?: string[];
  363. name?: string;
  364. title?: string;
  365. image?: string;
  366. summary?: string;
  367. };
  368. }
  369. /**
  370. * 将 ContentCollectionItem 转换为 ContentData
  371. */
  372. function convertToContentData(item: any): ContentData {
  373. console.log("Raw item:", item); // 添加原始数据日志
  374. // 直接从原始数据中获取字段
  375. const converted = {
  376. path: item._path || "",
  377. name: item.name || item.title || "",
  378. title: item.title || "",
  379. usage: item.meta.usage || [],
  380. capacities: item.meta.capacities || [],
  381. categoryId: item.meta.categoryId || "",
  382. category: item.meta.category || "",
  383. description: item.description || "",
  384. summary: item.meta.summary || "",
  385. image: item.meta.image || "",
  386. gallery: item.meta.gallery || [],
  387. body: item.body || "",
  388. content: item.body || null,
  389. series: item.meta.series || [],
  390. };
  391. console.log("Converting item:", item); // 添加日志
  392. console.log("Converted result:", converted); // 添加日志
  393. return converted;
  394. }
  395. /**
  396. * 加载产品详情
  397. */
  398. async function loadProduct() {
  399. try {
  400. const id = route.params.id as string;
  401. if (!id) {
  402. throw new Error("Product ID is required");
  403. }
  404. const { data } = await useAsyncData(`product-${id}`, () =>
  405. queryCollection("content")
  406. .where("path", "LIKE", `/products/${locale.value}/${id}`)
  407. .first()
  408. );
  409. if (!data.value) {
  410. throw new Error("Product not found");
  411. }
  412. const rawData = data.value as unknown as any;
  413. console.log("Raw product data:", rawData); // 添加日志
  414. const productData = convertToContentData(rawData);
  415. console.log("Converted product data:", productData); // 添加日志
  416. // 获取分类信息
  417. const { data: categoryData } = await useAsyncData(
  418. `category-${productData.categoryId}`,
  419. () =>
  420. queryCollection("content")
  421. .where(
  422. "path",
  423. "LIKE",
  424. `/categories/${locale.value}/${productData.categoryId}`
  425. )
  426. .first()
  427. );
  428. console.log("Category data:", categoryData.value); // 添加日志
  429. const categoryItem = categoryData.value
  430. ? convertToContentData(categoryData.value as unknown as any)
  431. : null;
  432. console.log("Converted category data:", categoryItem); // 添加日志
  433. // 设置产品数据,添加默认值
  434. product.value = {
  435. id: id,
  436. name: productData.name || productData.title || "",
  437. title: productData.title || productData.name || "",
  438. usage: productData.usage || [],
  439. capacities: productData.capacities || [],
  440. category: categoryItem?.title || productData.category || "",
  441. description: productData.description || "",
  442. summary: productData.summary || "",
  443. image: productData.image || "",
  444. gallery: productData.gallery || [],
  445. body: productData.body || "",
  446. content: productData.content || null,
  447. meta: {
  448. series: productData.meta?.series || [],
  449. name: productData.name,
  450. title: productData.title,
  451. image: productData.image,
  452. summary: productData.summary,
  453. },
  454. };
  455. console.log("Final product data:", product.value); // 添加日志
  456. // 设置当前图片
  457. if (product.value?.image) {
  458. currentImage.value = product.value.image;
  459. }
  460. // 加载相关产品
  461. await loadRelatedProducts();
  462. } catch (err) {
  463. console.error("Error loading product:", err);
  464. error.value = new Error(t("products.loadError"));
  465. }
  466. }
  467. /**
  468. * 预加载下一张图片
  469. */
  470. function preloadNextImage(image: string) {
  471. preloadImage.value = image;
  472. }
  473. /**
  474. * 处理预加载完成
  475. */
  476. function handlePreloadComplete() {
  477. preloadImage.value = null;
  478. }
  479. /**
  480. * 处理图片加载完成
  481. */
  482. function handleImageLoad() {
  483. isImageLoading.value = false;
  484. imageError.value = false;
  485. }
  486. /**
  487. * 处理图片加载错误
  488. */
  489. function handleImageError() {
  490. isImageLoading.value = false;
  491. imageError.value = true;
  492. }
  493. /**
  494. * 重试加载图片
  495. */
  496. function retryLoadImage() {
  497. isImageLoading.value = true;
  498. imageError.value = false;
  499. // 强制重新加载图片
  500. const img = new Image();
  501. img.src = currentImage.value;
  502. img.onload = () => {
  503. handleImageLoad();
  504. };
  505. img.onerror = () => {
  506. handleImageError();
  507. };
  508. }
  509. /**
  510. * 重试加载缩略图
  511. */
  512. function retryLoadThumbnail(index: number) {
  513. isThumbnailLoading.value[index] = true;
  514. thumbnailErrors.value[index] = false;
  515. // 强制重新加载缩略图
  516. const img = new Image();
  517. const images = [product.value?.image, ...(product.value?.gallery || [])];
  518. img.src = images[index] || "";
  519. img.onload = () => {
  520. handleThumbnailLoad(index);
  521. };
  522. img.onerror = () => {
  523. handleThumbnailError(index);
  524. };
  525. }
  526. /**
  527. * 处理缩略图加载完成
  528. */
  529. function handleThumbnailLoad(index: number) {
  530. isThumbnailLoading.value[index] = false;
  531. thumbnailErrors.value[index] = false;
  532. }
  533. /**
  534. * 处理缩略图加载错误
  535. */
  536. function handleThumbnailError(index: number) {
  537. isThumbnailLoading.value[index] = false;
  538. thumbnailErrors.value[index] = true;
  539. }
  540. /**
  541. * 切换图片
  542. */
  543. function changeImage(image: string | undefined) {
  544. if (image && image !== currentImage.value) {
  545. isImageLoading.value = true;
  546. imageError.value = false;
  547. preloadNextImage(image);
  548. currentImage.value = image;
  549. }
  550. }
  551. /**
  552. * 加载相关产品
  553. */
  554. async function loadRelatedProducts() {
  555. try {
  556. if (!product.value) return;
  557. const { data } = await useAsyncData(
  558. `related-products-${product.value.id}`,
  559. () =>
  560. queryCollection("content")
  561. .where("path", "LIKE", `/products/${locale.value}/%`)
  562. .all()
  563. );
  564. if (!data.value) return;
  565. const relatedItems = (data.value as unknown as ContentCollectionItem[]).map(
  566. (item) => convertToContentData(item)
  567. );
  568. relatedProducts.value = relatedItems
  569. .filter(
  570. (item) => item.path !== `/products/${locale.value}/${product.value?.id}`
  571. )
  572. .map((item) => ({
  573. id: item.path.split("/").pop() || "",
  574. name: item.name || item.title || "",
  575. title: item.title || item.name || "",
  576. usage: item.usage || [],
  577. capacities: item.capacities || [],
  578. category: "",
  579. description: item.description || "",
  580. summary: item.summary || "",
  581. image: item.image || "",
  582. gallery: item.gallery || [],
  583. body: item.body || "",
  584. meta: {
  585. series: item.meta?.series || [],
  586. name: item.name,
  587. title: item.title,
  588. image: item.image,
  589. summary: item.summary,
  590. },
  591. }));
  592. } catch (err) {
  593. console.error("Error loading related products:", err);
  594. }
  595. }
  596. // 页面加载时获取产品数据
  597. onMounted(() => {
  598. loadProduct();
  599. // 初始化缩略图加载状态数组
  600. isThumbnailLoading.value = Array(4).fill(true);
  601. thumbnailErrors.value = Array(4).fill(false);
  602. });
  603. // 监听路由参数变化
  604. watch(
  605. () => route.params.id,
  606. async (newId) => {
  607. if (newId) {
  608. await loadProduct();
  609. }
  610. },
  611. { immediate: true }
  612. );
  613. // 监听语言变化
  614. watch(
  615. () => locale.value,
  616. async () => {
  617. if (route.params.id) {
  618. await loadProduct();
  619. }
  620. }
  621. );
  622. // 监听产品数据变化,加载相关产品
  623. watch(
  624. () => product.value,
  625. () => {
  626. if (product.value) {
  627. loadRelatedProducts();
  628. }
  629. }
  630. );
  631. // SEO优化
  632. useHead(() => ({
  633. title: `${product.value?.name || "产品详情"} - Hanye`,
  634. meta: [
  635. {
  636. name: "description",
  637. content: product.value?.description || "产品详情页面",
  638. },
  639. ],
  640. }));
  641. </script>
  642. <style scoped>
  643. /* 隐藏滚动条但保持滚动功能 */
  644. .scrollbar-hide {
  645. -ms-overflow-style: none; /* IE and Edge */
  646. scrollbar-width: none; /* Firefox */
  647. }
  648. .scrollbar-hide::-webkit-scrollbar {
  649. display: none; /* Chrome, Safari and Opera */
  650. }
  651. /* 图片过渡动画 */
  652. .main-image {
  653. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  654. }
  655. /* 缩略图悬停效果 */
  656. .thumbnail-item {
  657. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  658. }
  659. .thumbnail-item:hover {
  660. transform: translateY(-2px);
  661. }
  662. /* 缩略图选中效果 */
  663. .thumbnail-item.selected {
  664. transform: scale(1.05);
  665. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
  666. 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  667. }
  668. /* 产品信息卡片效果 */
  669. .info-card {
  670. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  671. }
  672. .info-card:hover {
  673. transform: translateY(-2px);
  674. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
  675. 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  676. }
  677. /* 添加 prose 样式 */
  678. .prose {
  679. @apply text-stone-400;
  680. }
  681. .prose h1,
  682. .prose h2,
  683. .prose h3,
  684. .prose h4,
  685. .prose h5,
  686. .prose h6 {
  687. @apply text-white font-medium;
  688. }
  689. .prose a {
  690. @apply text-blue-400 hover:text-blue-300;
  691. }
  692. .prose ul,
  693. .prose ol {
  694. @apply list-disc list-inside;
  695. }
  696. .prose blockquote {
  697. @apply border-l-4 border-zinc-700 pl-4 italic;
  698. }
  699. .prose code {
  700. @apply bg-zinc-800 px-1 py-0.5 rounded;
  701. }
  702. .prose pre {
  703. @apply bg-zinc-800 p-4 rounded-lg overflow-x-auto;
  704. }
  705. .prose img {
  706. @apply rounded-lg;
  707. }
  708. </style>